
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: AbstractReader.java 301 2010-01-18 22:56:09Z archie.cobbs $
 */

package org.dellroad.sidekar.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dellroad.sidekar.schema.model.ArrayFieldModel;
import org.dellroad.sidekar.schema.model.AtomicFieldModel;
import org.dellroad.sidekar.schema.model.EntityModel;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.sidekar.schema.model.ReferenceFieldModel;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.schema.model.SimpleFieldModel;
import org.dellroad.stuff.java.Primitive;

/**
 * Support superclass for annotation scanners.
 */
abstract class AbstractReader {

    protected final Logger log = Logger.getLogger(getClass());

    protected SchemaModel schemaModel;
    protected Map<String, FieldModel> fieldMap;
    protected Map<Method, FieldModel> methodMap;
    protected Map<Class<?>, EntityModel> classMap;
    protected Set<Method> allMethods;

    /**
     * Configure information about the class we're working with.
     */
    public void configure(SchemaModel schemaModel, Map<Class<?>, EntityModel> classMap, Map<String, FieldModel> fieldMap,
      Map<Method, FieldModel> methodMap, Set<Method> allMethods) {
        this.schemaModel = schemaModel;
        this.classMap = classMap;
        this.fieldMap = fieldMap;
        this.methodMap = methodMap;
        this.allMethods = allMethods;
    }

    /**
     * Scan methods for schema annotations.
     */
    public abstract void process(Class<?> entityClass, Set<Method> methods);

    /**
     * Record a new {@link FieldModel} object.
     *
     * @throws InvalidAnnotationException if annotation is invalid
     */
    protected void recordModel(Annotation a, Method method, String name, FieldModel fieldModel) {

        // Validate name
        if (name.equals(""))
            throw new InvalidAnnotationException(a, method, "invalid empty field name \"" + name + "\"");

        // Check for duplicate field
        if (this.fieldMap.containsKey(name))
            throw new InvalidAnnotationException(a, method, "duplicate field \"" + name + "\"");

        // Configure field name
        fieldModel.setName(name);

        // Update maps
        this.fieldMap.put(name, fieldModel);
        if (this.methodMap.put(method, fieldModel) != null)
            throw new InvalidAnnotationException(a, method, "method has more than one schema annotation");
    }

    /**
     * Record a new {@link FieldModel} object.
     * This version assumes the method is a getter, and uses the property name as the default field name.
     *
     * @param method property getter method
     * @return {@link BeanProperty} for the Java bean property associated with {@code method}
     * @throws InvalidAnnotationException if annotation is invalid
     */
    protected BeanProperty recordGetterModel(Annotation a, Method method, String name, FieldModel fieldModel) {

        // Check parameter count
        checkParameterCount(a, method, 0);

        // Get property info
        BeanProperty prop;
        try {
            prop = new BeanProperty(method, this.allMethods);
        } catch (IllegalArgumentException e) {
            throw new InvalidAnnotationException(a, method, "method is not a getter method: " + e.getMessage());
        }

        // Default name if necessary
        if (name.length() == 0)
            name = prop.getName();

        // Create model
        recordModel(a, method, name, fieldModel);

        // Done
        return prop;
    }

    /**
     * Get the {@link SimpleFieldModel} class corresponding to the given Java {@link Type}.
     * If there is no direct correspondence, we check superclasses as well.
     *
     * @param type field's Java type
     * @param allowWrapper allow "wrapper" types to substitute for corresponding primitive types
     * @throws InvalidAnnotationException if type cannot be determined
     */
    protected SimpleFieldModel createSimpleField(Annotation a, Method method, Type type, boolean allowWrapper) {

        // Convert parameterized type to raw class
        Class<?> rclass = getRawType(a, method, type);

        // Handle array types
        if (rclass.isArray()) {
            Class<?> elemType = rclass.getComponentType();
            SimpleFieldModel elemModel = this.createSimpleField(a, method, elemType, false);
            if (!(elemModel instanceof AtomicFieldModel))
                throw new InvalidAnnotationException(a, method, "unsuppored array element type " + elemType);
            AtomicFieldModel atomicElemModel = (AtomicFieldModel)elemModel;
            ArrayFieldModel fieldModel = new ArrayFieldModel();
            fieldModel.setElementModel(atomicElemModel);
            return fieldModel;
        }

        // Map "wrapper" types to primitive types (if allowed)
        if (allowWrapper && !rclass.isPrimitive()) {
            Primitive primitive = Primitive.get(rclass);
            if (primitive != null)
                rclass = primitive.getType();
        }

        // Handle atomic types (including primitive types)
        try {
            return AtomicFieldModel.getModelType(rclass).newInstance();
        } catch (IllegalArgumentException e) {
            // no corresponding atomic field
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        // Field must be a reference field referring to referent
        ReferenceFieldModel fieldModel = new ReferenceFieldModel();
        fieldModel.setReferentModel(findEntityModelByType(a, method, rclass));
        return fieldModel;
    }

    /**
     * Check return type.
     *
     * @param exact whether type must match exactly or just be a superclass
     * @throws InvalidAnnotationException if return type does not match
     */
    protected void checkReturnType(Annotation a, Method method, Class<?> type, boolean exact) {
        if (exact && !method.getReturnType().equals(type))
            throw new InvalidAnnotationException(a, method, "method does not return " + type.getName());
        if (!getRawType(a, method, method.getGenericReturnType()).isAssignableFrom(type))
            throw new InvalidAnnotationException(a, method, "method does not return " + type);
    }

    /**
     * Check parameter count.
     *
     * @throws InvalidAnnotationException if parameter count is not exactly equal to {@code num}
     */
    protected void checkParameterCount(Annotation a, Method method, int num) {
        Type[] ptypes = method.getGenericParameterTypes();
        if (ptypes.length != num)
            throw new InvalidAnnotationException(a, method, "method does not take exactly " + num + " parameter(s)");
    }

    /**
     * Check duplicate method.
     *
     * @throws InvalidAnnotationException if {@code otherMethod} is not null
     */
    protected void checkDuplicateMethod(Annotation a, Method method, Method otherMethod) {
        if (otherMethod != null) {
            throw new InvalidAnnotationException(a, method, "duplicate annotation (also exists on method "
              + otherMethod + ")");
        }
    }

    /**
     * Get raw type, which must exist.
     *
     * @throws InvalidAnnotationException if {@code type} does not have a raw type
     */
    protected Class<?> getRawType(Annotation a, Method method, Type type) {
        if (type instanceof Class)
            return (Class<?>)type;
        if (type instanceof ParameterizedType) {
            Type rawType = ((ParameterizedType)type).getRawType();
            if (rawType instanceof Class)
                return (Class<?>)rawType;
        }
        throw new InvalidAnnotationException(a, method, "can't decipher generic type " + type);
    }

    /**
     * Find entity type corresponding to the given type.
     *
     * @throws InvalidAnnotationException if {@code type} does not have a raw type
     *  or the raw type cannot be resolved into an entity
     */
    protected EntityModel findEntityModelByType(Annotation a, Method method, Type type) {
        return findEntityModelByType(a, method, getRawType(a, method, type));
    }

    /**
     * Find entity type corresponding to the given class.
     *
     * @throws InvalidAnnotationException if {@code cl} does not correspond to an entity
     */
    protected EntityModel findEntityModelByType(Annotation a, Method method, Class<?> cl) {
        for (Class<?> sc = cl; sc != null; sc = sc.getSuperclass()) {
            EntityModel entityModel = this.classMap.get(sc);
            if (entityModel != null)
                return entityModel;
        }
        throw new InvalidAnnotationException(a, method, cl + " does not correspond to any known entity type");
    }
}

